Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / desktopApp / src / main / kotlin / org / meshtastic / desktop / notification / LinuxNotificationSender.kt

Displaying Raw • Download

desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt fix/qr-error-correction (7b45f84f) Text, 9.28 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.desktop.notification

Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787com.sun.jna.Library
Tff7b72import T7ee787com.sun.jna.Native
Tff7b72import T7ee787com.sun.jna.Pointer
Tff7b72import T7ee787com.sun.jna.Structure
Tff7b72import T7ee787com.sun.jna.ptr.PointerByReference
Tff7b72import T7ee787org.meshtastic.core.repository.Notification

T8b949e/**
* JNA bindings for libnotify (libnotify.so / libnotify-4.so).
*
* Only the minimal API surface needed for fire-and-forget desktop notifications is exposed. See:
* https://developer-old.gnome.org/libnotify/stable/
*
* **The GLib entry points below MUST stay on this interface.** They are deliberately resolved through the *libnotify*
* handle rather than a separate `Native.load("gobject-2.0", ...)`. `dlsym()` on a library handle searches that library
* *and its dependency chain*, and libnotify has a `DT_NEEDED` on `libgobject-2.0.so.0`/`libglib-2.0.so.0` — so this
* guarantees we get the exact same GLib instance that libnotify itself is bound to.
*
* Loading `gobject-2.0` separately is NOT safe: a JVM process can easily have two distinct GLib copies mapped at once
* (e.g. the JDK/Compose Desktop AWT stack drags in a bundled GTK3 + GLib, while libnotify binds to the system GLib).
* Each copy owns a private `GType` registry, so freeing a `NotifyNotification` created by one copy with the other
* copy's `g_object_unref()` walks the wrong registry and jumps through a null vtable slot — an immediate SIGSEGV at
* `pc=0x0` inside `g_object_unref`, not a catchable exception.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffFunctionNamingTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffFunctionParameterNamingTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffktlint:standard:function-namingTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72interface T56d364LibNotify Tb4b4b4: Te6edf3Library Tb4b4b4{
Tff7b72fun Td2a8ffnotify_initTb4b4b4(Te6edf3app_nameTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657Boolean

Tff7b72fun Td2a8ffnotify_notification_newTb4b4b4(Te6edf3summaryTb4b4b4: Tffa657StringTb4b4b4, Te6edf3bodyTb4b4b4: Tffa657String?Tb4b4b4, Te6edf3iconTb4b4b4: Tffa657String?Tb4b4b4)Tb4b4b4: Te6edf3Pointer?

Tff7b72fun Td2a8ffnotify_notification_set_urgencyTb4b4b4(Te6edf3notificationTb4b4b4: Te6edf3PointerTb4b4b4, Te6edf3urgencyTb4b4b4: Tffa657IntTb4b4b4)

Tff7b72fun Td2a8ffnotify_notification_set_categoryTb4b4b4(Te6edf3notificationTb4b4b4: Te6edf3PointerTb4b4b4, Te6edf3categoryTb4b4b4: Tffa657StringTb4b4b4)

Tff7b72fun Td2a8ffnotify_notification_set_hintTb4b4b4(Te6edf3notificationTb4b4b4: Te6edf3PointerTb4b4b4, Te6edf3keyTb4b4b4: Tffa657StringTb4b4b4, Te6edf3valueTb4b4b4: Te6edf3Pointer?Tb4b4b4)

Tff7b72fun Td2a8ffnotify_notification_showTb4b4b4(Te6edf3notificationTb4b4b4: Te6edf3PointerTb4b4b4, Te6edf3errorTb4b4b4: Te6edf3PointerByReference?Tb4b4b4)Tb4b4b4: Tffa657Boolean

Tff7b72fun Td2a8ffnotify_uninitTb4b4b4(Tb4b4b4)

T8b949e// --- GLib, resolved via libnotify's own dependency chain. See the KDoc above before touching these. ---

Tff7b72fun Td2a8ffg_object_unrefTb4b4b4(Te6edf3objTb4b4b4: Te6edf3PointerTb4b4b4)

Tff7b72fun Td2a8ffg_error_freeTb4b4b4(Te6edf3errorTb4b4b4: Te6edf3PointerTb4b4b4)

Tff7b72fun Td2a8ffg_variant_new_booleanTb4b4b4(Te6edf3valueTb4b4b4: Tffa657BooleanTb4b4b4)Tb4b4b4: Te6edf3Pointer

Tff7b72fun Td2a8ffg_variant_new_stringTb4b4b4(Te6edf3stringTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3Pointer
Tb4b4b4}

T8b949e/** JNA mapping of GLib's `GError` struct for extracting error diagnostics from libnotify. */
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Tf0883e@Structure.FieldOrderTb4b4b4(Ta5d6ff"Ta5d6ffdomainTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffcodeTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffmessageTa5d6ff"Tb4b4b4)
Tff7b72class T56d364GErrorStructTb4b4b4(Te6edf3pTb4b4b4: Te6edf3Pointer?Tb4b4b4) Tb4b4b4: Te6edf3StructureTb4b4b4(Te6edf3pTb4b4b4) Tb4b4b4{
Tf0883e@JvmField Tff7b72var Te6edf3domainTb4b4b4: Tffa657Int Tff7b72= T79c0ff0

Tf0883e@JvmField Tff7b72var Te6edf3codeTb4b4b4: Tffa657Int Tff7b72= T79c0ff0

Tf0883e@JvmField Tff7b72var Te6edf3messageTb4b4b4: Te6edf3Pointer? Tff7b72= Tff7b72null

Tff7b72init Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3p Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Te6edf3readTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72val Te6edf3errorMessageTb4b4b4: Tffa657String
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3messageTff7b72?.Te6edf3getStringTb4b4b4(T79c0ff0Tb4b4b4) Tff7b72?: Ta5d6ff"Ta5d6ffunknown errorTa5d6ff"
Tb4b4b4}

T8b949e/** libnotify urgency levels matching `NotifyUrgency` enum. */
Tff7b72private Tff7b72object T56d364NotifyUrgency Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3LOW Tff7b72= T79c0ff0
Tff7b72const Tff7b72val Te6edf3NORMAL Tff7b72= T79c0ff1
Tff7b72const Tff7b72val Te6edf3CRITICAL Tff7b72= T79c0ff2
Tb4b4b4}

T8b949e/**
* Sends notifications via libnotify on Linux, called directly through JNA.
*
* This avoids shelling out to `notify-send` and gives direct access to the notification daemon via D-Bus, providing
* proper urgency, category, and sound suppression support.
*
* Requires `libnotify` (typically `libnotify4` or `libnotify.so.4`) to be installed on the system. Falls back
* gracefully if the library cannot be loaded.
*/
Tff7b72class T56d364LinuxNotificationSenderTb4b4b4(
Tff7b72private Tff7b72val Te6edf3appNameTb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ffMeshtasticTa5d6ff"Tb4b4b4,
Tff7b72private Tff7b72val Te6edf3desktopEntryTb4b4b4: Tffa657String Tff7b72= Te6edf3appNameTb4b4b4.Te6edf3lowercaseTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3NativeNotificationSenderTb4b4b4,
Te6edf3AutoCloseable Tb4b4b4{

T8b949e/**
* Cleared by [close]. Volatile so a [close] on the shutdown path is visible to any thread that is about to call
* [send]; a send already past its null check will still complete against the old handle, which is why [close] is
* documented as "once sends have stopped".
*/
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3libTb4b4b4: Te6edf3LibNotify?

Tff7b72init Tb4b4b4{
Tff7b72var Te6edf3loadedLibTb4b4b4: Te6edf3LibNotify? Tff7b72= Tff7b72null
Tff7b72try Tb4b4b4{
Te6edf3loadedLib Tff7b72= Te6edf3NativeTb4b4b4.Te6edf3loadTb4b4b4(Ta5d6ff"Ta5d6ffnotifyTa5d6ff"Tb4b4b4, Te6edf3LibNotifyTff7b72::Te6edf3classTb4b4b4.Te6edf3javaTb4b4b4) Tff7b72as Te6edf3LibNotify
Tff7b72if Tb4b4b4(Te6edf3loadedLibTb4b4b4.Te6edf3notify_initTb4b4b4(Te6edf3appNameTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6fflibnotify initialized for 'Tffd700$Te6edf3appNameTa5d6ff'Ta5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffnotify_init('Tffd700$Te6edf3appNameTa5d6ff') returned falseTa5d6ff" Tb4b4b4}
Te6edf3loadedLib Tff7b72= Tff7b72null
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3UnsatisfiedLinkErrorTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6fflibnotify not available — native Linux notifications disabledTa5d6ff" Tb4b4b4}
Te6edf3loadedLib Tff7b72= Tff7b72null
Tb4b4b4}
Te6edf3lib Tff7b72= Te6edf3loadedLib
Tb4b4b4}

T8b949e/** Whether libnotify was successfully loaded and initialized. */
Tff7b72val Te6edf3isAvailableTb4b4b4: Tffa657Boolean
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3lib Tff7b72!Tff7b72= Tff7b72null

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffReturnCountTa5d6ff"Tb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffsendTb4b4b4(Te6edf3notificationTb4b4b4: Te6edf3NotificationTb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72val Te6edf3libnotify Tff7b72= Te6edf3lib Tff7b72?: Tff7b72return Tff7b72false

Tff7b72val Te6edf3ptr Tff7b72=
Te6edf3libnotifyTb4b4b4.Te6edf3notify_notification_newTb4b4b4(
Te6edf3notificationTb4b4b4.Te6edf3titleTb4b4b4,
Te6edf3notificationTb4b4b4.Te6edf3messageTb4b4b4,
Tff7b72nullTb4b4b4, T8b949e// icon — could be set to an app icon path in the future
Tb4b4b4)
Tff7b72?: Te6edf3run Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffnotify_notification_new returned nullTa5d6ff" Tb4b4b4}
Tff7b72return Tff7b72false
Tb4b4b4}

Te6edf3applyMetadataTb4b4b4(Te6edf3libnotifyTb4b4b4, Te6edf3ptrTb4b4b4, Te6edf3notificationTb4b4b4)

Tff7b72val Te6edf3errorRef Tff7b72= Te6edf3PointerByReferenceTb4b4b4(Tb4b4b4)
Tff7b72return Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3shown Tff7b72= Te6edf3libnotifyTb4b4b4.Te6edf3notify_notification_showTb4b4b4(Te6edf3ptrTb4b4b4, Te6edf3errorRefTb4b4b4)
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3shownTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3errMsg Tff7b72= Te6edf3errorRefTb4b4b4.Te6edf3valueTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3GErrorStructTb4b4b4(Tffa657itTb4b4b4)Tb4b4b4.Te6edf3errorMessage Tb4b4b4} Tff7b72?: Ta5d6ff"Ta5d6ffunknownTa5d6ff"
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffnotify_notification_show failed for 'Tffd700${Te6edf3notificationTb4b4b4.Te6edf3titleTffd700}Ta5d6ff': Tffd700$Te6edf3errMsgTa5d6ff" Tb4b4b4}
Tb4b4b4}
Te6edf3shown
Tb4b4b4} Tff7b72finally Tb4b4b4{
T8b949e// On failure libnotify hands us ownership of a GError; nothing else frees it, so without this every
T8b949e// failed send leaks the struct and its message. Freed here rather than in the `if (!shown)` branch so
T8b949e// it is released even if `show()` reports success while still having set an error. The message is read
T8b949e// above, before the free — GErrorStruct dereferences the pointer.
Te6edf3errorRefTb4b4b4.Te6edf3valueTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3libnotifyTb4b4b4.Te6edf3g_error_freeTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Te6edf3libnotifyTb4b4b4.Te6edf3g_object_unrefTb4b4b4(Te6edf3ptrTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Releases libnotify's process-wide state — the D-Bus proxy and cached app name allocated by `notify_init()`.
*
* Idempotent: the second and later calls are no-ops. Afterwards [isAvailable] is false and [send] returns false
* rather than calling into a torn-down library.
*
* Call this once sends have stopped. `notify_uninit()` is resolved through the libnotify handle, so it runs against
* the same GLib instance that allocated the state — see the [LibNotify] KDoc for why that matters.
*/
Tf0883e@Synchronized
Tff7b72override Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3libnotify Tff7b72= Te6edf3lib Tff7b72?: Tff7b72return
Te6edf3lib Tff7b72= Tff7b72null
Te6edf3runCatching Tb4b4b4{ Te6edf3libnotifyTb4b4b4.Te6edf3notify_uninitTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffnotify_uninit() failed during shutdownTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffapplyMetadataTb4b4b4(Te6edf3libnotifyTb4b4b4: Te6edf3LibNotifyTb4b4b4, Te6edf3ptrTb4b4b4: Te6edf3PointerTb4b4b4, Te6edf3notificationTb4b4b4: Te6edf3NotificationTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3urgency Tff7b72=
Tff7b72when Tb4b4b4(Te6edf3notificationTb4b4b4.Te6edf3typeTb4b4b4) Tb4b4b4{
Te6edf3NotificationTb4b4b4.Te6edf3TypeTb4b4b4.Te6edf3Error Tff7b72-Tff7b72> Te6edf3NotifyUrgencyTb4b4b4.Te6edf3CRITICAL
Te6edf3NotificationTb4b4b4.Te6edf3TypeTb4b4b4.Te6edf3Warning Tff7b72-Tff7b72> Te6edf3NotifyUrgencyTb4b4b4.Te6edf3NORMAL
Tff7b72else Tff7b72-Tff7b72> Te6edf3NotifyUrgencyTb4b4b4.Te6edf3LOW
Tb4b4b4}
Te6edf3libnotifyTb4b4b4.Te6edf3notify_notification_set_urgencyTb4b4b4(Te6edf3ptrTb4b4b4, Te6edf3urgencyTb4b4b4)

Tff7b72val Te6edf3category Tff7b72=
Tff7b72when Tb4b4b4(Te6edf3notificationTb4b4b4.Te6edf3categoryTb4b4b4) Tb4b4b4{
Te6edf3NotificationTb4b4b4.Te6edf3CategoryTb4b4b4.Te6edf3Message Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffim.receivedTa5d6ff"
Te6edf3NotificationTb4b4b4.Te6edf3CategoryTb4b4b4.Te6edf3Battery Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffdevice.warningTa5d6ff"
Te6edf3NotificationTb4b4b4.Te6edf3CategoryTb4b4b4.Te6edf3Alert Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffdevice.errorTa5d6ff"
Te6edf3NotificationTb4b4b4.Te6edf3CategoryTb4b4b4.Te6edf3NodeEvent Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffnetworkTa5d6ff"
Te6edf3NotificationTb4b4b4.Te6edf3CategoryTb4b4b4.Te6edf3MeshBeacon Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffnetworkTa5d6ff"
Te6edf3NotificationTb4b4b4.Te6edf3CategoryTb4b4b4.Te6edf3Service Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffdeviceTa5d6ff"
Tb4b4b4}
Te6edf3libnotifyTb4b4b4.Te6edf3notify_notification_set_categoryTb4b4b4(Te6edf3ptrTb4b4b4, Te6edf3categoryTb4b4b4)

T8b949e// desktop-entry hint associates notifications with the app's .desktop file,
T8b949e// enabling proper icon resolution and notification grouping by the daemon.
T8b949e// The GVariants below are floating refs; notify_notification_set_hint() sinks them, so we must not unref.
Te6edf3libnotifyTb4b4b4.Te6edf3notify_notification_set_hintTb4b4b4(Te6edf3ptrTb4b4b4, Ta5d6ff"Ta5d6ffdesktop-entryTa5d6ff"Tb4b4b4, Te6edf3libnotifyTb4b4b4.Te6edf3g_variant_new_stringTb4b4b4(Te6edf3desktopEntryTb4b4b4)Tb4b4b4)

Tff7b72if Tb4b4b4(Te6edf3notificationTb4b4b4.Te6edf3isSilentTb4b4b4) Tb4b4b4{
Te6edf3libnotifyTb4b4b4.Te6edf3notify_notification_set_hintTb4b4b4(Te6edf3ptrTb4b4b4, Ta5d6ff"Ta5d6ffsuppress-soundTa5d6ff"Tb4b4b4, Te6edf3libnotifyTb4b4b4.Te6edf3g_variant_new_booleanTb4b4b4(Tff7b72trueTb4b4b4)Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.16s